﻿using System;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.Common;

using RAD.LightOne.Cache;
using RAD.LightOne.Interfaces;

namespace RAD.LightOne.Data
{
#if OLD_MODE
    #region Documentation
    /// <summary>
    /// Classe que provê funcionalidades de acesso a banco de dados e operções no banco.
    /// </summary>
    #endregion Documentation
    public sealed class DBEngine : IDisposable, IDbConnection, IDbCommand
    {
        #region Fields

        private static DbProviderFactory factory;
        private static ConnectionStringSettings setting;

        private MyDbTransaction trx;

        #region IDbConnection required

        private IDbConnection conn;

        #endregion

        #region IDbCommand required

        private IDbCommand command;

        #endregion

        #endregion Fields

        #region Properties

        public static DbProviderFactory Factory
        {
            get
            {
                return DBEngine.factory;
            }
        }

        public ConnectionStringSettings Setting
        {
            get
            {
                return DBEngine.setting;
            }
        }

        #region Documentation
        /// <summary>
        /// Retorna se o DBEngine é transacionado ou não, ou seja, se foi criado recebendo uma transação.
        /// </summary>
        #endregion Documentation
        public bool IsTransactioned
        {
            get
            {
                return (this.command.Transaction != null);
            }
        }

        #endregion Properties

        #region Constructors

        static DBEngine()
        {
            try
            {
                string stdConnName = System.Configuration.ConfigurationManager.AppSettings["StdConnection"];
                setting = System.Configuration.ConfigurationManager.ConnectionStrings[stdConnName];
                factory = DbProviderFactories.GetFactory(setting.ProviderName);
            }
            catch (Exception ex)
            {
                throw new ConfigurationErrorsException("The app.config or web.config, don't be cofigured correctly.", ex);
            }
        }

        #region Documentation
        /// <summary>
        /// Construtor que em si já abre a conexão a ser utilizada, que é o próprio DBEngine.
        /// </summary>
        #endregion Documentation
        private DBEngine()
        {
            try
            {
                System.Threading.Thread loadAssemblies = new System.Threading.Thread(new System.Threading.ThreadStart(TypesPool.Load));
                loadAssemblies.Start();

                this.conn = factory.CreateConnection();
                this.conn.ConnectionString = setting.ConnectionString;
                this.conn.Open();
                this.command = this.Connection.CreateCommand();
            }
            catch (Exception ex)
            {
                throw new ConfigurationErrorsException("LightOne unable to open the connection with the passed connection string.", ex);
            }
        }

        #region Documentation
        /// <summary>
        /// Construtor para operações transacionadas.
        /// </summary>
        /// <param name="trx">transação corrente.</param>
        #endregion
        private DBEngine(IDbTransaction trx) 
        {
            this.conn = trx.Connection;
            this.command = this.conn.CreateCommand();
            this.Transaction = trx;
        }

        #region Documentation
        /// <summary>
        /// Destrutor de DBEngine.
        /// </summary>
        #endregion Dcoumentation
        ~DBEngine() 
        {
            this.command = null;
            this.conn = null;
        }
        #endregion Constructors

        #region Methods

        public static DBEngine GetInstance()
        {
            return new DBEngine();
        }

        public static DBEngine GetInstance(IDbTransaction trx)
        {
            return new DBEngine(trx);
        }

        #region Documentation
        /// <summary>
        /// Recebe uma instrução sql e os parâmetros que devem ser concatenados a ela e 
        /// prepara o comando para ser executado.
        /// </summary>
        /// <param name="sql">instrução sql.</param>
        /// <param name="args">argumentos a concatenar</param>
        #endregion
        private void Prepare(string sql, params object[] args)
        {
            this.Parameters.Clear();
            if (args.Length == 0)
            {
                this.CommandText = sql;
            }
            else
            {
                List<string> paramsNames = new List<string>();
                for (int i = 0; i < args.Length; i++)
                {
                    if (!sql.Contains("{" + i.ToString() + "}")) throw new IndexOutOfRangeException("MyObject.Prepare(sql, args).\nÍndice do argumento inexistente na expressão sql.");
                    IDataParameter param = this.command.CreateParameter();
                    param.ParameterName = Data.ParameterPrefix.Prefix + i.ToString();
                    if (args[i] is IMyObject)
                    {
                        param.Value = ((IMyObject)args[i]).Id;
                    }
                    else
                    {
                        param.Value = args[i];
                    }
                    paramsNames.Add(param.ParameterName);
                    this.command.Parameters.Add(param);
                }
                try
                {
                    StringBuilder paramParser = new StringBuilder();
                    this.CommandText = paramParser.AppendFormat(sql, paramsNames.ToArray()).ToString();
                }
                catch (Exception except)
                {
                    throw new IndexOutOfRangeException("MyObject.Prepare(sql, args).\n" + except.Message, except);
                }
            }
            
        }

        #region Documentation
        /// <summary>
        /// Preenche um DataTable com o schema resultante do comando SQl "Select" passado
        /// ao DBEngine.
        /// </summary>
        /// <param name="table">DataTable que será preenchido com dados</param>
        #endregion
        public void FillSchema(DataTable table)
        {
            try
            {
                DbDataAdapter adapt = DBEngine.Factory.CreateDataAdapter();
                adapt.SelectCommand = (DbCommand)this.command;
                adapt.SelectCommand.Prepare();
                adapt.FillSchema(table, SchemaType.Source);
            }
            catch (Exception ex)
            {
                throw new InvalidProgramException("Unable to get the table schema.", ex);
            }
        }

        #region Documentation
        /// <summary>
        /// Executa um comando de retorno único, como count, sum , etc... comportamento
        /// transacionado ou não, de acordo com o construtor da classe.
        /// </summary>
        /// <param name="sql">instrução sql parametrizada com parâmetros {1..n}.</param>
        /// <param name="args">argumentos dos parâmetros.</param>
        #endregion
        public object ExecuteScalar(string sql, params object[] args)
        {
            this.Prepare(sql, args);
            return this.command.ExecuteScalar();
        }

        #region Documentation
        /// <summary>
        /// Executa um IDataReader e o retorna, transacionado ou não, de acordo com o construtor da classe.
        /// </summary>
        /// <param name="sql">instrução sql parametrizada com parâmetros {1..n}.</param>
        /// <param name="args">argumentos dos parâmetros.</param>
        #endregion
        public IDataReader ExecuteReader(string sql, params object[] args)
        {
            this.Prepare(sql, args);
            return this.ExecuteReader();
        }

        #region Documentation
        /// <summary>
        /// Executa uma instrução sql sem retorno, "não select" e retorna o número de registros afetados.
        /// </summary>
        /// <param name="sql">instrução sql a ser executada.</param>
        /// <param name="args">argumentos a serem concatenados.</param>
        #endregion
        public int ExecuteNonQuery(string sql, params object[] args)
        {
            this.Prepare(sql.ToString(), args);
            return this.command.ExecuteNonQuery();
        }

        #region Documentation
        /// <summary>
        /// Insere os dados, persiste em banco, os dados da instância passada.
        /// </summary>
        /// <param name="instance">instância a ser persistida.</param>
        /// <param name="trx">transação de banco utilizada para a operação.</param>
        #endregion
        internal void Insert(MyObject instance, IDbTransaction trx)
        {
            instance.RefreshLastPercistence();

            InternalObj item = InternalObjBuilder.GetWriteStructure<MyObject>(instance, DataOperations.Insert);
            InternalObjProcessor.Standardize(item, trx);
            foreach (Table tItem in item)
            {
                StringBuilder sql = new StringBuilder();

                this.Parameters.Clear();

                StringBuilder fields = new StringBuilder();
                StringBuilder parms = new StringBuilder();

                foreach (Field fItem in tItem)
                {
                    fields.Append((fields.Length == 0 ? "" + fItem.Name : "," + fItem.Name));
                    parms.Append((parms.Length == 0 ? "" + fItem.GetParameterName() : "," + fItem.GetParameterName()));

                    IDbDataParameter param = this.CreateParameter();
                    param.ParameterName = fItem.GetParameterName();
                    param.Value = fItem.Value;
                    this.Parameters.Add(param);
                }
                sql.AppendFormat("INSERT INTO {0}({1}) VALUES({2})", tItem.Name, fields.ToString(), parms.ToString());
                this.CommandText = sql.ToString();
                this.ExecuteNonQuery();
            }

        }

        #region Documentation
        /// <summary>
        /// Atualiza os dados de uma instância no banco de dados.
        /// </summary>
        /// <param name="instance">instância que será atualizada.</param>
        #endregion Documentation
        internal void Update(MyObject instance, IDbTransaction trx)
        {
            DateTime previousPersistence = instance.LastPersistence;
            instance.RefreshLastPercistence();

            this.Transaction = trx;

            bool optimisticFieldOut = true;
            bool idFinded = false;

            InternalObj item = InternalObjBuilder.GetWriteStructure<MyObject>(instance, DataOperations.Insert);
            InternalObjProcessor.Standardize(item, trx);
            foreach (Table tItem in item)
            {
                StringBuilder preSql = new StringBuilder("UPDATE {0} SET {1} WHERE id={2}");
                this.Parameters.Clear();

                StringBuilder fields = new StringBuilder();

                foreach (Field fItem in tItem)
                {
                    if (optimisticFieldOut && fItem.Name.ToLower() == "last_persistence")
                    {
                        Field lastPercistence = new Field("last_persistence_verify");
                        lastPercistence.Value = previousPersistence;
                        preSql.Append(" AND last_persistence=" + lastPercistence.GetParameterName());
                        IDataParameter verifyParam = this.CreateParameter();
                        verifyParam.ParameterName = lastPercistence.GetParameterName();
                        verifyParam.Value = lastPercistence.Value;
                        this.Parameters.Add(verifyParam);
                        optimisticFieldOut = false;
                    }
                    if (!idFinded && fItem.Name.ToLower() == "id")
                    {
                        preSql.Replace("{2}", fItem.GetParameterName());
                        idFinded = !idFinded;
                    }

                    fields.Append((fields.Length == 0 ? "" + fItem.Name + "=" + fItem.GetParameterName() : "," + fItem.Name + "=" + fItem.GetParameterName()));

                    IDbDataParameter param = this.CreateParameter();
                    param.ParameterName = fItem.GetParameterName();
                    param.Value = fItem.Value;
                    this.Parameters.Add(param);
                }

                if(optimisticFieldOut) throw new InvalidProgramException("The type " + instance.GetType().ToString() + " don't have a last_persistence field in your hierarchy. Only one is needed.");

                StringBuilder sql = new StringBuilder();
                sql.AppendFormat(preSql.ToString(), tItem.Name, fields.ToString(), instance.Id.ToString());
                this.CommandText = sql.ToString();
                this.ExecuteNonQuery();
                idFinded = !idFinded;
            }


        }

        #region Documentation
        /// <summary>
        /// Exclui uma instância do banco de dados.
        /// </summary>
        /// <param name="instance">instância a ser excluída.</param>
        #endregion Documentation
        internal void Delete(MyObject instance, IDbTransaction trx)
        {
            StringBuilder sql = new StringBuilder();

            this.Parameters.Clear();
            this.Transaction = trx;

            IDbDataParameter param = this.CreateParameter();
            param.ParameterName = new Field("id").GetParameterName();
            param.Value = instance.Id.ToString();
            this.Parameters.Add(param);

            this.CommandText = sql.AppendFormat("DELETE FROM {0} WHERE id={1}", MyObjectMetadataReader.ExtractMainTable(instance.GetType()), param.ParameterName).ToString();
            this.ExecuteNonQuery();
        }

        #region Documentation
        /// <summary>
        /// PERIGO! Exclui todos os dados relacionados ao tipo passado.
        /// </summary>
        /// <param name="type">tipo a ser excluído. deve herdar de MyObject.</param>
        #endregion Documentation
        [System.Diagnostics.Conditional("DEBUG")]
        public void DeleteAll(Type type)
        {
            if (!type.IsSubclassOf(typeof(MyObject))) throw new ArgumentException("DBEngine.DeleteAll().Type passed don't be a subclass of MyObject.");
            try
            {
                StringBuilder sql = new StringBuilder("DELETE FROM {0}");
                foreach (string name in MyObjectMetadataReader.GetSignTables(type))
                {
                    sql.AppendFormat(sql.ToString(), MyObjectMetadataReader.GetSignTables(type));

                    this.CommandText = sql.ToString();
                    this.ExecuteNonQuery();
                }
            }
            catch (Exception excep)
            {
                throw new InvalidOperationException("DBEngine.DeleteAll().", excep);
            }
        }
        #endregion Methods

        #region IDisposable Members

        public void Dispose()
        {
            this.command.Dispose();
            this.conn.Dispose();
        }

        #endregion

        #region IDbConnection Members

        public IDbTransaction BeginTransaction(IsolationLevel il)
        {
            return new MyDbTransaction(this.conn.BeginTransaction(il));
        }

        public IDbTransaction BeginTransaction()
        {
            return new MyDbTransaction(this.conn.BeginTransaction());
        }

        [Obsolete("This method cannot be invoked, the database selection stay in the config file.",true)]
        public void ChangeDatabase(string databaseName)
        {
            //implementar um pool de servidores
        }

        public void Close()
        {
            this.conn.Close();
        }

        public string ConnectionString
        {
            get
            {
                return this.conn.ConnectionString;
            }
            set
            {
                throw new Exception("The property is readonly.");
            }
        }

        public int ConnectionTimeout
        {
            get { return this.conn.ConnectionTimeout; }
        }

        #region Documentation
        /// <summary>
        /// Retorna um IDbCommand "puro" baseado no provider utilizado, sem adicionais do LightOne.
        /// Para ter acesso aos recursos extras do Light, utilize diretamente o DBEngine.
        /// </summary>
        #endregion Documentation
        public IDbCommand CreateCommand()
        {
            return this.conn.CreateCommand();
        }

        public string Database
        {
            get { return this.conn.Database; }
        }

        public void Open()
        {
            this.conn.Open();
        }

        public ConnectionState State
        {
            get { return this.conn.State; }
        }

        #endregion

        #region IDbCommand Members

        public void Cancel()
        {
            this.command.Cancel();
        }

        public string CommandText
        {
            get
            {
                return this.command.CommandText;
            }
            set
            {
                this.command.CommandText = value;
            }
        }

        public int CommandTimeout
        {
            get
            {
                return this.command.CommandTimeout;
            }
            set
            {
                this.command.CommandTimeout = value;
            }
        }

        public CommandType CommandType
        {
            get
            {
                return this.command.CommandType;
            }
            set
            {
                this.command.CommandType = value;
            }
        }

        public IDbConnection Connection
        {
            get
            {
                return this;
            }
            set
            {
                this.conn = value;
            }
        }

        public IDbDataParameter CreateParameter()
        {
            return this.command.CreateParameter();
        }
        
        public int ExecuteNonQuery()
        {
            return this.command.ExecuteNonQuery();
        }

        public IDataReader ExecuteReader(CommandBehavior behavior)
        {
            return this.command.ExecuteReader(behavior);
        }

        public IDataReader ExecuteReader()
        {
            return this.command.ExecuteReader();
        }

        public object ExecuteScalar()
        {
            return this.command.ExecuteScalar();
        }

        public IDataParameterCollection Parameters
        {
            get { return this.command.Parameters; }
        }

        public void Prepare()
        {
            this.command.Prepare();
        }

        public IDbTransaction Transaction
        {
            get
            {
                return this.trx;
            }
            set
            {
                if (value is MyDbTransaction)
                {
                    this.command.Transaction = ((MyDbTransaction)value).InnerTransaction;
                    this.trx = value as MyDbTransaction;
                }
                else
                {
                    this.command.Transaction = value;
                    this.trx = new MyDbTransaction(value);
                }
            }
        }

        public UpdateRowSource UpdatedRowSource
        {
            get
            {
                return this.command.UpdatedRowSource;
            }
            set
            {
                this.command.UpdatedRowSource = value;
            }
        }

        #endregion

    }
#endif
}
